using System;
using PoolKit.Strategies.Repairing;
using PoolKit.Strategies.ReusePreparation;

namespace PoolKit
{
    /// <summary>
    /// Pool that is the foundation of all pools, a constructor.
    /// 
    /// Note: this class is thread-safe.
    /// </summary>
    /// <remarks>The pool can be starved. If you want a pool that cannot be starved, use the <see cref="PoolToleratingStarvation{TResource}"/> wrapper</remarks>
    /// <typeparam name="TResource"></typeparam>
    internal class GenericPool<TResource> : MutablePool<TResource>
        where TResource: class
	{
        private readonly object _stateChangeCriticalSection = new object();
        private readonly IResourceCollection<TResource> _resourceCollection;
        private readonly ICommandProcessor _commandProcessor;
        private readonly ReusePreparationStrategy<TResource> _reusePreparationStrategy;
        private readonly RepairStrategy<TResource> _repairStrategy;
        private readonly DisposableCookie _disposableCookie = new DisposableCookie(Resources.PoolDisposed);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="resourceCollection"></param>
        /// <param name="reusePreparationStrategy"></param>
        /// <param name="repairStrategy"></param>
        /// <param name="commandProcessor"></param>
        /// <exception cref="ArgumentNullException"></exception>
        internal GenericPool(
            IResourceCollection<TResource> resourceCollection,
            ReusePreparationStrategy<TResource> reusePreparationStrategy,
            RepairStrategy<TResource> repairStrategy,
            ICommandProcessor commandProcessor)
        {
            if (resourceCollection == null) throw new ArgumentNullException("resourceCollection");
            if (commandProcessor == null) throw new ArgumentNullException("commandProcessor");
            if (reusePreparationStrategy == null) throw new ArgumentNullException("reusePreparationStrategy");
            if (repairStrategy == null) throw new ArgumentNullException("repairStrategy");

            _resourceCollection = resourceCollection;
            _commandProcessor = commandProcessor;

            _reusePreparationStrategy = reusePreparationStrategy.ToThreadSafe();

            _repairStrategy = repairStrategy.ToThreadSafe();
        }

        /// <summary>
        /// Asks the pool to lend you a resource
        /// </summary>
        /// <returns>
        /// A resource borrowed from pool
        /// </returns>
        /// <exception cref="CannotLendResourceException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public override TResource Lend()
		{
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                TResource resource;

                if (!TryLend(out resource))
                {
                    throw new CannotLendResourceException(Resources.PoolExhausted);
                }

                return resource;
            }
		}

        /// <summary>
        /// Asks the pool to give up a resource to you - after this the pool no longer tracks the
        /// resource and you cannot return it to the pool.
        /// </summary>
        /// <returns>
        /// A resource from pool.
        /// </returns>
        /// <exception cref="CannotGiveUpResourceException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public override TResource GiveUp()
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                TResource resource;

                if (!_resourceCollection.TryTake(out resource))
                {
                    throw new CannotGiveUpResourceException(Resources.PoolExhausted);
                }

                try
                {
                    _resourceCollection.Forget(resource);
                }
                catch (CannotForgetResourceException)
                {
                    _resourceCollection.AcceptBack(resource);
                    throw new CannotGiveUpResourceException(Resources.CannotGiveUpResourceInternalError);
                }
                
                return resource;
            }
        }

        /// <summary>
        /// Asks the pool to lend you a resource
        /// </summary>
        /// <param name="resource">A resource borrowed from pool</param>
        /// <returns>True, if the resource has been lent, False - otherwise</returns>
        /// <exception cref="ObjectDisposedException"></exception>
        internal override bool TryLend(out TResource resource)
		{
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                return _resourceCollection.TryTake(out resource);
            }
		}

        /// <summary>
        /// Returns a previously borrowed resource to the pool
        /// </summary>
        /// <param name="resource">A previously borrowed resource to be returned</param>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public override void AcceptBack(TResource resource)
		{
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                ReserveRightToReturnResource(resource);

                var command = new PrepareForReuseCommand<TResource>(_reusePreparationStrategy, _resourceCollection, resource);

                _commandProcessor.EnqueueCommand(command);
            }
		}

        /// <summary>
        /// Returns a previously borrowed, broken resource to the pool
        /// </summary>
        /// <param name="resource">Broken resource to be returned.</param>
        /// <exception cref="ObjectDisposedException"></exception>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public override void AcceptBackBroken(TResource resource)
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                ReserveRightToReturnResource(resource);

                var command = new RepairAndPrepareForReuseCommand<TResource>(_reusePreparationStrategy,
                                                                             _resourceCollection,
                                                                             resource,
                                                                             _repairStrategy);

                _commandProcessor.EnqueueCommand(command);
            }
		}

        /// <exception cref="CannotAcceptResourceException"></exception>
        private void ReserveRightToReturnResource(TResource resource)
        {
            if (resource == null)
            {
                throw new ArgumentNullException("resource");
            }

            try
            {
                _resourceCollection.ReserveRightToReturnResource(resource);
            }
            catch (InvalidOperationException exception)
            {
                throw new CannotAcceptResourceException(exception.Message);
            }
        }

        /// <summary>
        /// Registers a resource that has already been lent with the pool
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="ObjectDisposedException"></exception>
        /// <exception cref="CannotAddResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public override void RegisterLentResource(TResource resource)
        {
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                if (resource == null) throw new ArgumentNullException("resource");

                _resourceCollection.RegisterResourceAsTaken(resource);
            }
        }

        ~GenericPool()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool calledFromFinalizer)
        {
            lock (_stateChangeCriticalSection)
            {
                if (_disposableCookie.IsDisposed)
                {
                    return;
                }

                _commandProcessor.Dispose();

                _disposableCookie.MarkAsDisposed();

                _resourceCollection.Dispose();
            }
        }

        /// <summary>
        /// Disposes of the pool
        /// </summary>
        public override void Dispose()
		{
            Dispose(false);
            GC.SuppressFinalize(this);
		}
	}
}